package edu.upf.grupoe.p2ppicture.SearchEngine.implementations;

import edu.upf.grupoe.p2ppicture.DBAccessEngine.implementations.DBActionImpl;
import edu.upf.grupoe.p2ppicture.DBAccessEngine.implementations.DBActionResultImpl;
import edu.upf.grupoe.p2ppicture.DBAccessEngine.implementations.DBManagerImpl;
import edu.upf.grupoe.p2ppicture.DBAccessEngine.implementations.Photo;
import edu.upf.grupoe.p2ppicture.SearchEngine.interfaces.InstantMessage;
import java.io.File;
import java.io.InvalidClassException;
import java.util.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.codec.EncoderException;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smackx.filetransfer.FileTransferListener;
import org.jivesoftware.smackx.filetransfer.FileTransferManager;
import org.jivesoftware.smackx.filetransfer.FileTransferRequest;
import org.jivesoftware.smackx.filetransfer.IncomingFileTransfer;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.exist.xmldb.DatabaseImpl;

/**
 * This class represents a connection and all the features of it.
 */

public class InstantMessageImpl extends Observable implements InstantMessage, edu.upf.grupoe.p2ppicture.SearchEngine.interfaces.Constants{

    private Connection connection = null;

    private String user="";
    private String password = "";
    private String resource = "";

    private MultiUserChat chat = null;
    private Roster roster = null;
    private PacketFilter filter = null;
    private ListenerImpl listener = null;

    private GenericMessageImpl msg = null;

    private static Log logger = LogFactory.getLog("Main");

    private static InstantMessageImpl INSTANCE = null;

    private SearchProfileImpl SP = new SearchProfileImpl(null, null);
    private SearchResultImpl SR = new SearchResultImpl(null);
    private UserContentImpl UC = new UserContentImpl(null, null);
    
    private List<Packet> Inbox = new ArrayList<Packet>();
    private HashMap<String,String> Requests = new HashMap<String, String>(50);
    // SR answer. Observable
    
    private String folder = "received";

    /**
     * InstantMessageImpl constructor.
     * @param connection Connection
     * @param chat MultiUserChat
     */

    public InstantMessageImpl(Connection connection, MultiUserChat chat) {
        this.connection = connection;
        this.chat = chat;
    }
    /**
     * Connection Singleton
     */

    private static void createInstance() {
        if (INSTANCE == null) {
            INSTANCE = new InstantMessageImpl(null,null);
        }
    }
    /**
     * Connection Singleton
     * @return Connection
     */
    public static InstantMessageImpl getInstance() {
        if (INSTANCE == null) createInstance();
        return INSTANCE;
    }
    /**
     * This method connects a user to a session and configures the filter, the packetcollector and the chat
     * @param user String
     * @param password String
     * @param resource String
     * @throws NullPointerException
     * @throws XMPPException
     */
    public void login(String user, String password, String resource) throws  NullPointerException, XMPPException{
        if(user.isEmpty() || password.isEmpty()){
            NullPointerException npe = new NullPointerException("Empty User or Password fields");
            if(logger.isDebugEnabled()){
                logger.error(npe.getMessage());
            }
            throw npe;
        }
        else if(resource.isEmpty()){
            resource = RESOURCE;
        }

        ConnectionConfiguration config = new ConnectionConfiguration("talk.google.com", 5222, "gmail.com");
        
        config.setSASLAuthenticationEnabled(false);
        config.setReconnectionAllowed(false);

        connection = new XMPPConnection(config);
        connection.connect();
        // Login
        connection.login(user,password,resource);
        if(logger.isDebugEnabled()){
            logger.info("Connected as: "+connection.getUser());
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ex) {
            if(logger.isDebugEnabled()){
                logger.error("Sleep interrupted: "+ex.getMessage());
            }
        }
        if(connection.isAuthenticated()){
            filter = new AndFilter((new PacketTypeFilter(Message.class)));

            roster = connection.getRoster();
            roster.addRosterListener(new RosterListener() {

                public void entriesAdded(java.util.Collection<String> clctn) {                
                }

                public void entriesUpdated(java.util.Collection<String> clctn) {   
                }

                public void entriesDeleted(java.util.Collection<String> clctn) {
                }

                public void presenceChanged(Presence prsnc) {
                    String user = prsnc.getFrom();
                    logger.info(user+ " has changed his presence");
                    setChanged();
                    notifyObservers();
                }
            });

            connection.createPacketCollector(filter);
            chat = new MultiUserChat(connection,user+"'s Chat");
            listener = new ListenerImpl(this);
            connection.addPacketListener((PacketListener) new ListenerImpl(this), filter);
        }
    }
    /**
     * This method disconnects a user from a session
     */
    public void logout() {
        if(connection != null) {
            connection.disconnect();
        }
        if(logger.isDebugEnabled()){
            logger.info("Disconnecting...");
        }
    }
    /**
     * This method makes a search in a database of a content provided by the user and generates a SearchProfile message (SP) with that information.
     * @param to String
     * @param typeOfSearch String
     * @param keywords List<String>
     * @throws XMPPException
     * @throws NullPointerException
     */
    public void search(List<String> to, String typeOfSearch, List<String> keywords) throws XMPPException, NullPointerException{
        SP = new SearchProfileImpl(keywords, typeOfSearch);
        if(to.isEmpty() || typeOfSearch.isEmpty() || keywords.isEmpty()) {
            NullPointerException npe = new NullPointerException();
            if(logger.isDebugEnabled()){
                logger.error(npe.getMessage());
            }
            throw npe;
        }
        user = connection.getUser();
        Iterator itr = to.iterator();
        while(itr.hasNext()){
        msg = new GenericMessageImpl((String)itr.next()+"/"+RESOURCE, user, SEARCHPROFILE, SP);
            try{
                chat.sendMessage(msg);
            }
            catch(XMPPException xe){
                if(logger.isDebugEnabled()){
                    logger.error("Error while the message was being sent" + xe.getMessage());
                }
                throw xe;
            }
            if(logger.isDebugEnabled()){
                logger.info("SP message sent to "+msg.getTo());
            }
        }
    }
    /**
     * This method process a SP query and responds with a SearchResult message (SR) with a list of contents and their description
     * @param packet Packet
     * @param msg GenericMessageImpl
     * @throws XMPPException
     * @throws InvalidClassException
     */
    public void offerContent(Packet packet, GenericMessageImpl msg) throws XMPPException, InvalidClassException {
        if(packet.getProperty(SEARCHPROFILE) instanceof SearchProfileImpl){
            SP = (SearchProfileImpl) packet.getProperty(SEARCHPROFILE);
            Iterator itr = SP.getKeywords().iterator();
            String action = SP.getTypeOfSearch();
            
            if(logger.isDebugEnabled()){
                logger.info("SP received from "+packet.getFrom());
                logger.info("SP was for "+packet.getTo());
                //logger.info("SP Type of Search: "+action);
                //logger.info("SP Keywords: ");
            }
            String [] keywords = new String[SP.getKeywords().size()];
            SP.getKeywords().toArray(keywords);
            DBActionImpl dba = new DBActionImpl(action);
            dba.setParameters(keywords);

            DBManagerImpl manager = DBManagerImpl.getInstance();
            try{
                // Query to the database
                DBActionResultImpl dbari = manager.execute(dba);
                SR = new SearchResultImpl(null);
                // Fitting results in a SR
                SR = converToSR(dbari);
            }
            catch(Exception e){
                logger.info("Error:" +e.getMessage());
            }
            
            if(logger.isDebugEnabled() && (SR.getContents() != null) ){
                logger.info("SR created");
            }
            // send SR
            msg = new GenericMessageImpl(packet.getFrom(), packet.getTo(),SEARCHRESULT, SR);
            
            try {
                    chat.sendMessage(msg);
                    if(logger.isDebugEnabled()){
                        logger.info("SR message has been sent to "+msg.getTo()+" from: "+msg.getFrom());                     
                    }

            } catch (XMPPException ex) {
                    if(logger.isDebugEnabled()){
                        logger.error("SR message has not been sent");
                    }
                    throw ex;
            }
        }
        else{
            InvalidClassException ice = new InvalidClassException("Invalid Class Received, SearchProfile Needed");
            throw ice;
        }
    }
    /**
     * This method processes a SearchResult message and generates a UserContent message (UC) with the user final choice from the different possibilities
     * @param packet Packet
     * @param msg GenericMessageImpl
     * @throws XMPPException
     * @throws InvalidClassException
     */
    public void getContent(Packet packet, GenericMessageImpl msg) throws XMPPException, InvalidClassException{
            //SR = (SearchResultImpl) packet.getProperty(SEARCHRESULT);
            Inbox.add(packet);
            logger.info("SR Inbox has changed");
            // Observable
            setChanged();
            notifyObservers();        
    }
    public void requestContent(String content, String user) throws XMPPException{
        UC = new UserContentImpl(user,content);
        msg = new GenericMessageImpl(user, connection.getUser(), USERCONTENT, UC);
        chat.sendMessage(msg);
    }
    /**
     * This method processes a UC and returns a Confirmation message which indicates that the transfer process is starting
     * @param packet Packet
     * @param msg GenericMessageImpl
     * @throws XMPPException
     * @throws InvalidClassException
     */
    /**
     * This method returns a connection
     * @return Connection
     */
    public Connection getConnection(){
        return connection;
    }
    /**
     * This method returns a MultiUserChat
     * @return MultiUserChat
     */
    public MultiUserChat getChat(){
        return chat;
    }
    /**
     * This method returns a GenericMessageImpl
     * @return GenericMessageImpl
     */
    public GenericMessageImpl getGenericMessageImpl(){
        return msg;
    }
    /**
     * This method returns a SearchResult
     * @return SearchResultImpl
     */
    public SearchResultImpl getSR(){
        return SR;
    }
    /**
     * This method returns a SearchProfile
     * @return SearchProfileImpl
     */
    public SearchProfileImpl getSP(){
        return SP;
    }
    /**
     * This method returns a UserContent
     * @return UserContentImpl
     */
    public UserContentImpl getUC(){
        return UC;
    }
    /**
     * This method obtains the user list
     * @return Vector<String>
     */

    public Vector<String> getRosterEntries(){
        Vector<String> users = new Vector<String>();
        for (Iterator<RosterEntry> i = roster.getEntries().iterator(); i.hasNext();) {
            RosterEntry re = (RosterEntry) i.next();
            users.add(re.getUser());
        }
        return users;
    }
    /**
     * This method configures a FileTransferListener to a given connection FileTransferManager
     */
    public void initializeManager(){
         final FileTransferManager manager = new FileTransferManager(connection);
         manager.addFileTransferListener(new FileTransferListener() {
            @Override
            public void fileTransferRequest(FileTransferRequest request) {
                  logger.info("A file transfer Request has been detected");
                  IncomingFileTransfer transfer = request.accept();
                  File directory = new File(folder);
                  boolean exists = directory.exists();
                  if(!exists){
                      new File(folder).mkdir();
                      logger.info("Creating a folder");
                  }
                  File received = new File(folder+System.getProperty("file.separator")+request.getFileName());
                  if(received != null){
                        try {
                            transfer.recieveFile(received);
                        } catch (XMPPException ex) {
                            logger.error("Connection error while setting received file");
                        }
                    }
                    else{
                        logger.error("Error creating the incoming ");
                    }
            }
      });
    }
    public void setFolder(String str){
        this.folder = str;
    }
    public String getFolder(){
        return this.folder;
    }

    public void sendChatMessage(String message, String to){
        try {
            Message msg = new Message();
            msg.setTo(to);
            msg.setBody(message);
            chat.sendMessage(msg);
        } catch (XMPPException ex) {
            logger.error("Error sending the message: "+ex.getMessage());
        }
    }
    
    public ListenerImpl getListener(){
        return listener;
    }

    public List<Packet> getInbox(){
        return Inbox;
    }
    
    public HashMap<String,String> getRequests(){
        return Requests;
    }

    void getRequest(Packet packet) {
        UserContentImpl UCtemp = (UserContentImpl)packet.getProperty("UserContent");
        String content = UCtemp.getURL();
        String receiver = packet.getFrom();
        Requests.put(content,receiver); // Map<content->key,receiver->value>
        setChanged();
        notifyObservers();
    }

    private SearchResultImpl converToSR(DBActionResultImpl dbari) {
        SearchResultImpl SR = new SearchResultImpl(null);
        if(dbari.isOk()){
            Iterator itr = dbari.getResult().iterator();
            logger.info("Number of photos: "+dbari.getResult().size());
            List<ContentImpl> list = new ArrayList<ContentImpl>();
            ContentImpl content;
            Photo p;
            ThumbnailImpl th;
            while(itr.hasNext()){
                p = (Photo)itr.next();
                DescriptionContentImpl dc = new DescriptionContentImpl(p.getPath(),p.getKeywords());
                String path = p.getPath();
                logger.info("Photo "+path);
                th = new ThumbnailImpl(path);
                content = new ContentImpl(th, dc);
                list.add(content);
            }
            SR = new SearchResultImpl(list);
        }
        else{
            SR = new SearchResultImpl(null);
            logger.error("SR has not been created due to DBActionResultImpl has errors");
        }
        return SR;
    }
    public Vector<String> getConnectedUsers(){
        Vector<String> users = new Vector<String>();
        for (Iterator<RosterEntry> i = roster.getEntries().iterator(); i.hasNext();) {
            RosterEntry re = (RosterEntry) i.next();
            if(roster.getPresence(re.getUser()).getType() != Presence.Type.unavailable){
                users.add(re.getUser());
            }            
        }
        return users;
    }
    public void newInbox(){
        Inbox = new ArrayList<Packet>();
    }
}